package moe.dao.pool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ConnectionBuilder {
	private static Logger log = Logger.getLogger(ConnectionBuilder.class.getName());
	private final static int STATEMENT_POOL_SIZE = 20;
	private final String jdbcUrl, user, password;

	ConnectionBuilder(String driverClass, String jdbcUrl, String user, String password) {
		try {
			Class.forName(driverClass);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
		this.jdbcUrl = jdbcUrl;
		this.user = user;
		this.password = password;
	}

	Connection createConnection(Pool<Connection> pool) throws SQLException {
		return createConnectionProxy(pool);
	}

	Connection getPrincipalConnection(Object obj) {
		Connection result = null;
		if (Proxy.isProxyClass(obj.getClass())) {
			InvocationHandler handler = Proxy.getInvocationHandler(obj);
			if (handler instanceof ConnectionInvocationHandler) {
				result = ((ConnectionInvocationHandler) handler).getPrincipal();
			}
		}
		return result;
	}

	private Connection createConnection() throws SQLException {
		Connection conn = null;
		conn = DriverManager.getConnection(jdbcUrl, user, password);
		return conn;
	}

	private Connection createConnectionProxy(Pool<Connection> pool) throws SQLException {
		Connection conn = createConnection();
		ConnectionInvocationHandler handler = new ConnectionInvocationHandler(conn, pool);
		conn = (Connection) Proxy.newProxyInstance(
				getClass().getClassLoader(),
				new Class[] { Connection.class },
				handler);
		return conn;
	}

	private static enum Status {
		CHECKOUT, CHECKIN;
	}

	private static class ConnectionInvocationHandler implements InvocationHandler, Checkoutable {
		private Connection principal;
		private Pool<Connection> connectionPool;
		private Pool<Statement> statementPool;
		private LRUFixedSizeMap<String, Statement> preparedStatementPool, callableStatementPool;
		private Status status;

		ConnectionInvocationHandler(Connection conn, Pool<Connection> pool) {
			this.principal = conn;
			this.connectionPool = pool;
			status = Status.CHECKOUT;
			initStatementPools();
		}

		private void initStatementPools() {
			statementPool = new Pool<Statement>(STATEMENT_POOL_SIZE, 1,
					connectionPool.getAliveTimeout(), Integer.MAX_VALUE);
			preparedStatementPool = new LRUFixedSizeMap<String, Statement>(STATEMENT_POOL_SIZE);
			callableStatementPool = new LRUFixedSizeMap<String, Statement>(STATEMENT_POOL_SIZE);
		}

		public void checkout() {
			status = Status.CHECKOUT;
		}

		public Connection getPrincipal() {
			return principal;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			method.setAccessible(true);
			String methodName = method.getName();
			Object result = null;
			if (status == Status.CHECKIN) {
				if ("isClosed".equals(methodName)) {
					return true;
				} else if ("close".equals(methodName)) {
					clear();
					return method.invoke(principal, args);
				}
				throw new SQLException("Connection had be closed");
			} else if ("close".equals(methodName)) {
				status = Status.CHECKIN;
				connectionPool.checkin(new PoolEntity<Connection>((Connection) proxy,
						connectionPool));
			} else if ("createStatement".equals(methodName)
					|| "prepareStatement".equals(methodName)
					|| "prepareCall".equals(methodName)) {
				result = getStatement(proxy, method, args);
			} else {
				result = method.invoke(principal, args);
			}
			return result;
		}

		private void clear() {
			statementPool.clear();
			preparedStatementPool.clear();
			callableStatementPool.clear();
		}

		private Object getStatement(Object proxy, Method method, Object[] args)
				throws Throwable {
			Object result = getStatementFromPool(method.getName(), args);
			if (result == null) {
				result = createStatement(proxy, method, args);
			}
			return result;
		}

		private Object createStatement(Object proxy, Method method,
				Object[] args) throws Throwable {
			String methodName = method.getName();
			Object stat = method.invoke(principal, args);
			InvocationHandler handler = null;
			if ("createStatement".equals(methodName)) {
				handler = new StatementInvocationHandler((Statement) stat, (Connection) proxy,
						statementPool);
			} else if ("prepareStatement".equals(methodName)) {
				handler = new PreparedStatementInvocationHandler((Statement) stat,
						(Connection) proxy, preparedStatementPool, (String) args[0]);
			} else {
				handler = new PreparedStatementInvocationHandler((Statement) stat,
						(Connection) proxy, callableStatementPool, (String) args[0]);
			}
			return Proxy.newProxyInstance(
					getClass().getClassLoader(),
					new Class[] { CallableStatement.class },
					handler);
		}

		private Object getStatementFromPool(String methodName, Object[] args) {
			Object result = null;
			if ("createStatement".equals(methodName)) {
				result = getStatementFromPool();
			} else if ("prepareStatement".equals(methodName)) {
				result = getPrepareStatementFromPool(args);
			} else {
				result = getCallableStatementFromPool(args);
			}
			return result;
		}

		private Object getStatementFromPool() {
			int timeout = 15 * 1000;
			Object result = null;
			PoolEntity<Statement> entity = null;
			while (!statementPool.isEmpty()) {
				try {
					entity = statementPool.checkout(timeout, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
				if (entity != null) {
					result = entity.get();
					try {
						((Statement) result).getConnection();
					} catch (SQLException e) {
						result = null;
						continue;
					}
					if (log.isLoggable(Level.FINE)) {
						log.fine(principal.toString() + "\n\tbingo statement pool, size:"
								+ statementPool.size());
					}
					break;
				}
			}
			return result;
		}

		private Object getPrepareStatementFromPool(Object[] args) {
			Object result = preparedStatementPool.get(args[0]);
			if (log.isLoggable(Level.FINE)) {
				log.fine(principal.toString() + "\n\tprepared Statement Pool size: "
						+ preparedStatementPool.size());
			}
			if (result != null && Proxy.isProxyClass(result.getClass())) {
				((Checkoutable) Proxy.getInvocationHandler(result)).checkout();
				if (log.isLoggable(Level.FINE)) {
					log.fine("\tbingo prepared Statement Pool:" + args[0]);
				}
			} else {
				if (log.isLoggable(Level.FINE)) {
					log.fine("\tnot bingo prepared Statement Pool:" + args[0]);
				}
			}
			return result;
		}

		private Object getCallableStatementFromPool(Object[] args) {
			Object result = callableStatementPool.get(args[0]);
			if (result != null && Proxy.isProxyClass(result.getClass())) {
				((Checkoutable) Proxy.getInvocationHandler(result)).checkout();
			}
			return result;
		}
	}

	private static class StatementInvocationHandler implements InvocationHandler, Checkoutable {
		private Statement principal;
		private Connection proxyConnection;
		private Status status;
		private Pool<Statement> pool;

		protected StatementInvocationHandler(Statement stat, Connection proxy) {
			principal = stat;
			proxyConnection = proxy;
			status = Status.CHECKOUT;
		}

		public StatementInvocationHandler(Statement stat, Connection proxy, Pool<Statement> pool) {
			this(stat, proxy);
			this.pool = pool;
		}

		public void checkout() {
			status = Status.CHECKOUT;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			method.setAccessible(true);
			Object result = null;
			String methodName = method.getName();
			if (status == Status.CHECKIN) {
				if ("close".equals(methodName)) {
					return method.invoke(principal, args);
				}
				throw new SQLException("Statement had be closed");
			} else if ("getConnection".equals(methodName)) {
				method.invoke(principal, args);
				result = proxyConnection;
			} else if ("close".equals(methodName)) {
				status = Status.CHECKIN;
				doCheckin(proxy);
			} else {
				result = method.invoke(principal, args);
			}
			return result;
		}

		protected void doCheckin(Object proxy) {
			pool.checkin(new PoolEntity<Statement>((Statement) proxy, pool));
		}
	}

	private static class PreparedStatementInvocationHandler extends StatementInvocationHandler {
		private LRUFixedSizeMap<String, Statement> pool;
		private String sql;

		public PreparedStatementInvocationHandler(Statement stat, Connection proxy,
				LRUFixedSizeMap<String, Statement> pool, String sql) {
			super(stat, proxy);
			this.pool = pool;
			this.sql = sql;
		}

		@Override
		protected void doCheckin(Object proxy) {
			Object obj = pool.put(sql, (Statement) proxy);
			if (obj != null && obj != proxy) {
				try {
					((Statement) obj).close();
				} catch (SQLException e) {
					// ignore
				}
			}
		}
	}
}
